/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.workflow.model;

import java.lang.String;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.servlet.http.HttpServletRequest;
import offset.nodes.Constants;
import offset.nodes.server.error.model.ExceptionId;
import offset.nodes.server.error.model.LoggedException;
import offset.nodes.client.model.Severity;
import offset.nodes.server.model.Cache;
import offset.nodes.server.model.CacheObjectFactory;
import offset.nodes.server.model.RepositoryModel;
import offset.nodes.server.model.XPathQuery;
import offset.nodes.server.workflow.model.State.DisplayType;
import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
import org.apache.jackrabbit.spi.Name;

/**
 * A model class used to access the repository by the WorkflowAction.
 * 
 * @author Walter Lütgenau
 */
public class WorkflowModel extends RepositoryModel {

    public static final String CACHE_GLOBAL_WORKFLOW_ROLES = "workflowRoles";
    public static final String CACHE_GLOBAL_WORKFLOW = "globalWorkflow";
    public static final String CACHE_GLOBAL_WORKFLOW_DOCUMENT = "globalWorkflowDocument";
    public static ExceptionId EXCEPTION_WORKFLOW_ROLES_DETERMINATION = new ExceptionId("workflow.rolesDetermination", Severity.Info);

    public WorkflowModel(Session session) {
        super(session);
    }

    /**
     * Get a workflowDocument object from a workflow document node. If an object with the name equal to the UUID of the workflow document node
     * is already stored in the session, return that. If not, read a new workflow document from the repository, store it into the session and return it.
     *
     * @param workflowDocumentNode
     * @param request used to obtain the HttpSession
     * @return the workflow document object
     * @throws UnsupportedRepositoryOperationException
     * @throws RepositoryException
     */
    public WorkflowDocument getWorkflowDocument(Node workflowDocumentNode, HttpServletRequest request) throws UnsupportedRepositoryOperationException, RepositoryException {
        WorkflowDocument workflowDocument = (WorkflowDocument) Cache.getObject(request.getSession(), CACHE_GLOBAL_WORKFLOW_DOCUMENT+workflowDocumentNode.getUUID(),
                new WorkflowDocumentCacheObjectFactory(workflowDocumentNode.getUUID()));
        workflowDocument.getNode().refresh(true);
        return workflowDocument;
    }

    /**
     * Return a workflow object from the workflow node.
     *
     * @param workflowNode
     * @return the workflow object
     * @throws RepositoryException
     */
    public Workflow getWorkflow(Node workflowNode) throws RepositoryException {
        return getWorkflow(workflowNode.getProperty(Constants.JCR_UUID).getString());
    }

    /**
     * Return a workflow object from the workflow node.
     *
     * @param workflowNode
     * @return the workflow object
     * @throws RepositoryException
     */
    public Workflow getWorkflow(String workflowUUID) throws RepositoryException {
        Workflow workflow = (Workflow) Cache.getObject(CACHE_GLOBAL_WORKFLOW+workflowUUID, new WorkflowCacheObjectFactory(workflowUUID));

        return workflow;
    }

    /**
     * Can the session owner access the given state?
     *
     * @param state
     * @return
     */
    public boolean isPermitted(State state) {
        HashSet<String> workflowRoles = (HashSet<String>) Cache.getObject(CACHE_GLOBAL_WORKFLOW_ROLES + getSession().getUserID(), new UserRolesCacheObjectFactory());

        for (String roleUUID : state.getRoleUUIDs()) {
            if (workflowRoles.contains(roleUUID))
                return true;
        }

        return false;
    }

    /**
     * Return the creation path for the given virtual page instance. The method assumes, that the page is the topmost page of the template
     *
     * @param virtualPageInstanceUUID
     * @return the path part of the simple primary query
     * @throws RepositoryException
     */
    public String getCreationPath(String virtualPageInstanceUUID) throws RepositoryException {
        Node virtualPageInstanceNode = getNodeByUuid(virtualPageInstanceUUID);
        return virtualPageInstanceNode.getParent().getPath();
    }

    /**
     * Creates a workflow roles cache object, if necessary, for the generic cache utility.
     */
    class UserRolesCacheObjectFactory implements CacheObjectFactory {

        /**
         * Return the UUID of the current nodes:user
         *
         * @return the UUID
         * @throws RepositoryException
         */
        protected String getUserUUID() throws RepositoryException {
            String userId = getSession().getUserID();
            XPathQuery query = new XPathQuery(getSession());
            NodeIterator ni = query.executeQuery("//" + userId + "[@jcr:primaryType='" + Constants.TYPENAME_USER + "']");
            if (ni.hasNext())
                return ni.nextNode().getProperty(Constants.JCR_UUID).getString();

            return null;
        }

        /**
         *  Return the UUIDs of all nodes:role(s) assigned to the UUID of the specified user
         *
         * @param userUUID the UUID of the user
         * @return a HashSet<String> of all UUIDs of the roles assigned to the user
         * @throws RepositoryException
         */
        protected HashSet<String> getRoleUUIDs(String userUUID) throws RepositoryException {
            if (userUUID == null)
                return new HashSet<String>();
            
            XPathQuery query = new XPathQuery(getSession());
            NodeIterator ni = query.executeQuery("/jcr:root//element(*, " + Constants.TYPENAME_ROLE + ")[@" + Constants.PROP_USER_REFERENCE + "='" + userUUID + "']");
            HashSet<String> result = new HashSet<String>();
            for (int i = 0; ni.hasNext(); i++) {
                result.add(ni.nextNode().getUUID());
            }

            return result;

        }

        /**
         *
         * @see CacheObjectFactory
         */
        public Object createCacheObject() {
            try {
                return getRoleUUIDs(getUserUUID());
            } catch (RepositoryException ex) {
                throw new LoggedException(EXCEPTION_WORKFLOW_ROLES_DETERMINATION, ex);
            }

        }
    }

    /**
     * Create a workflow document object from a workflow document node and return it.
     *
     * @param workflowDocumentNode
     * @return the workflow document object
     * @throws RepositoryException
     */
    protected WorkflowDocument readWorkflowDocument(Node workflowDocumentNode) throws RepositoryException {
        String workflowUUID = workflowDocumentNode.getProperty(Constants.PROP_WORKFLOW).getString();
        Workflow workflow = (Workflow) Cache.getObject(CACHE_GLOBAL_WORKFLOW+workflowUUID,
                new WorkflowCacheObjectFactory(workflowUUID));
        WorkflowDocument document = new WorkflowDocument(workflowDocumentNode, workflow, workflowDocumentNode.getProperty(Constants.PROP_CURRENT_STATE).getString());

        return document;
    }

    /**
     * Create a workflow object from a workflow node and return it.
     *
     * @param workflowNode
     * @return the workflow object
     * @throws RepositoryException
     */
    protected Workflow readWorkflow(Node workflowNode) throws RepositoryException {
        Workflow workflow = new Workflow();

        workflow.setName(workflowNode.getName());
        workflow.setDescription(workflowNode.getProperty(Constants.PROP_DESCRIPTION).getString());

        NodeIterator si = workflowNode.getNodes();
        while (si.hasNext()) {
            Node child = si.nextNode();
            if (!child.isNodeType(Constants.TYPE_STATE))
                continue;

            State state = readState(child);

            workflow.getStates().put(state.getName(), state);
        }

        return workflow;

    }

    /**
     * Read a workflow node from the UUID, create a workflow object from the node and return the object.
     *
     * @param workflowUUID
     * @return the workflow object
     * @throws RepositoryException
     */
    protected Workflow readWorkflow(String workflowUUID) throws RepositoryException {
        Node workflowNode = getSession().getNodeByUUID(workflowUUID);
        return readWorkflow(workflowNode);
    }

    /**
     * Create a state object from the state node and return it.
     *
     * @param stateNode
     * @return the state object
     * @throws RepositoryException
     */
    protected State readState(Node stateNode) throws RepositoryException {
        State state = new State(stateNode.getName());

        if (stateNode.hasProperty(Constants.PROP_DESCRIPTION))
            state.setDescription(stateNode.getProperty(Constants.PROP_DESCRIPTION).getString());

        if (stateNode.hasProperty(Constants.PROP_NEW_STATE))
            state.setNewState(stateNode.getProperty(Constants.PROP_NEW_STATE).getBoolean());

        if (stateNode.hasNode(Constants.CHILD_TEMPLATE_EDITOR)) {
            state.setDisplayType(DisplayType.template);
            Node templateEditor = stateNode.getNode(Constants.CHILD_TEMPLATE_EDITOR);
            state.setVirtualPageInstanceUUID(templateEditor.getProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE).getString());
            Node virtualPageInstance = templateEditor.getProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE).getNode();
            if (virtualPageInstance.hasProperty(Constants.PROP_PRIMARY_QUERY))
                state.setTree(true);
            else
                state.setTree(false);
            if (templateEditor.hasProperty(Constants.PROP_DIRECT_EDIT))
                state.setDirectEdit(templateEditor.getProperty(Constants.PROP_DIRECT_EDIT).getBoolean());
            else
                state.setDirectEdit(false);
            if (templateEditor.hasProperty(Constants.PROP_RELATIVE_PATH))
                state.setRelativePath(templateEditor.getProperty(Constants.PROP_RELATIVE_PATH).getString());
        } else if (stateNode.hasNode(Constants.CHILD_BUILT_IN_EDITOR)) {
            state.setDisplayType(DisplayType.builtIn);
            Node builtInEditor = stateNode.getNode(Constants.CHILD_BUILT_IN_EDITOR);
            state.setRelativePath(builtInEditor.getProperty(Constants.PROP_RELATIVE_PATH).getString());
            state.setContentType(builtInEditor.getProperty(Constants.PROP_CONTENT_TYPE).getString());
            state.setDirectEdit(true);
        }

        if (stateNode.hasProperty(Constants.PROP_TRANSITION)) {
            Value[] transitions = stateNode.getProperty(Constants.PROP_TRANSITION).getValues();
            for (Value transitionUUID : transitions) {
                Node transitionNode = getSession().getNodeByUUID(transitionUUID.getString());

                state.getTransitionNames().add(transitionNode.getName());
            }
        }

        if (stateNode.hasProperty(Constants.PROP_ROLE)) {
            Value[] roles = stateNode.getProperty(Constants.PROP_ROLE).getValues();
            for (Value roleUUID : roles) {
                state.getRoleUUIDs().add(roleUUID.getString());
            }
        }

        return state;
    }

    /**
     * Check from the primary node type name, if this is a workflow document.
     *
     * @param primaryNodeType The name of the primary node type
     * @return The result of the check.
     * @throws NoSuchNodeTypeException
     */
    public boolean isWorkflowDocument(String primaryNodeType) throws NoSuchNodeTypeException {
        NodeTypeDef nodeTypeDef = getNodeTypeDef(primaryNodeType);
        if (nodeTypeDef.getName().equals(getName(Constants.TYPE_WORKFLOW_DOCUMENT)))
            return true;

        for (Name superType : nodeTypeDef.getSupertypes()) {
            if (isWorkflowDocument(superType))
                return true;
        }

        return false;

    }

    /**
     * Check from the primary node type name, if this is a workflow document.
     * 
     * @param primaryNodeType The name of the primary node type
     * @return The result of the check.
     * @throws NoSuchNodeTypeException
     */
    public boolean isWorkflowDocument(Name primaryNodeType) throws NoSuchNodeTypeException {
        NodeTypeDef nodeTypeDef = getNodeTypeDef(primaryNodeType);
        if (nodeTypeDef.getName().equals(getName(Constants.TYPE_WORKFLOW_DOCUMENT)))
            return true;

        for (Name superType : nodeTypeDef.getSupertypes()) {
            if (isWorkflowDocument(superType))
                return true;
        }

        return false;
    }

    /**
     * Create a workflow by its UUID for use in a cache.
     */
    class WorkflowCacheObjectFactory implements CacheObjectFactory {
        String workflowUUID;

        public WorkflowCacheObjectFactory(String workflowUUID) {
            this.workflowUUID = workflowUUID;
        }

        public Object createCacheObject() {
            try {
                return readWorkflow(getSession().getNodeByUUID(workflowUUID));
            } catch (RepositoryException ex) {
                Logger.getLogger(WorkflowModel.class.getName()).log(Level.SEVERE, null, ex);
                return null;
            }
        }

    }

    /**
     * Create a workflow by its UUID for use in a cache.
     */
    class WorkflowDocumentCacheObjectFactory implements CacheObjectFactory {
        String workflowDocumentUUID;

        public WorkflowDocumentCacheObjectFactory(String workflowDocumentUUID) {
            this.workflowDocumentUUID = workflowDocumentUUID;
        }

        public Object createCacheObject() {
            try {
                return readWorkflowDocument(getSession().getNodeByUUID(workflowDocumentUUID));
            } catch (RepositoryException ex) {
                Logger.getLogger(WorkflowModel.class.getName()).log(Level.SEVERE, null, ex);
                return null;
            }
        }

    }

    /**
     * Check, if this is in fact a workflow document. If not return null.
     * If it is, retrieve the workflow from the corresponding property. Retrieve the virtual page instance valid for the current state.
     * The latter can be determined from the corresponding property.
     *
     * Return the virtual page instance uuid.
     *
     * @param uuid The UUID of workflow document node.
     * @return The UUID of the virtual page instance of the current state.
     * @throws RepositoryException
     */
    public String getWorkflowVirtualPageInstanceUUID(String uuid) throws RepositoryException {
        Node workflowDocumentNode = getSession().getNodeByUUID(uuid);
        if (!workflowDocumentNode.isNodeType(Constants.TYPE_WORKFLOW_DOCUMENT))
            return null;

        String workflowUUID = workflowDocumentNode.getProperty(Constants.PROP_WORKFLOW).getString();
        Workflow workflow = (Workflow) Cache.getObject(CACHE_GLOBAL_WORKFLOW+workflowUUID, new WorkflowCacheObjectFactory(workflowUUID));

        String currentStateName = workflowDocumentNode.getProperty(Constants.PROP_CURRENT_STATE).getString();
        State currentState = workflow.getStates().get(currentStateName);

        return currentState.getVirtualPageInstanceUUID();
    }

    /**
     * Persist the new state, when a transition is done.
     * 
     * @param workflowDocument
     * @param newState
     * @throws RepositoryException
     */
    public void updateState(WorkflowDocument workflowDocument, State newState) throws RepositoryException {
        workflowDocument.getNode().setProperty(Constants.PROP_CURRENT_STATE, newState.getName());
        workflowDocument.setCurrentState(newState.getName());
        getSession().save();
    }

    /**
     * Is the given node a subnode of a workflow document node?
     * 
     * @param node
     * @return
     * @throws RepositoryException
     */
    public boolean isWorkflowDocumentSubNode(Node node) throws RepositoryException {
        Node root = getSession().getRootNode();
        while (!node.isSame(root)) {
            if (node.isNodeType(Constants.TYPE_WORKFLOW_DOCUMENT))
                return true;
            node = node.getParent();
        }

        return false;
    }

    /**
     * Find the ancestor workflow document of this subnode of a workflow document node.
     * 
     * @param subNode
     * @return the workflow document node
     * @throws RepositoryException
     */
    public Node getWorkflowDocumentNodeFromSubNode(Node subNode) throws RepositoryException {
        Node root = getSession().getRootNode();
        while (!subNode.isSame(root)) {
            if (subNode.isNodeType(Constants.TYPE_WORKFLOW_DOCUMENT))
                return subNode;
            subNode = subNode.getParent();
        }

        return null;

    }

}
